Repository navigation
Add Release workflow and prepare v0.1.0-alpha.1 - #45
Conversation
Introduce a GitHub Actions Release pipeline that builds Linux/macOS/Windows installers and publishes them as a GitHub Release (prerelease for alpha tags). Bump package and desktop shell versions to 0.1.0-alpha.1 and document how to trigger artifact builds vs formal releases. Co-authored-by: Damon <[email protected]>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe PR adds manual and tag-triggered release automation. It validates versions, tests the workspace, builds desktop installers for three platforms, smoke-tests the API sidecar, uploads artifacts, creates tags when needed, and publishes GitHub Releases. Package and desktop metadata now use ChangesRelease delivery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Release workflow
participant meta
participant desktop-build
participant API-sidecar
participant GitHub Release
Release workflow->>meta: resolve and validate release version
meta-->>desktop-build: provide version and prerelease metadata
desktop-build->>API-sidecar: bundle, launch, and poll health endpoint
API-sidecar-->>desktop-build: return health and workspace status
desktop-build-->>Release workflow: upload installer artifacts
Release workflow->>GitHub Release: create release and attach installers
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Reconcile alpha Release workflow docs with the new Desktop Installers workflow that replaced CI workflow_dispatch builds on main. Co-authored-by: Damon <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 21-22: Set the workflow-level permissions default to contents:
read, then add contents: write only within the publish job. Ensure the test and
desktop jobs inherit read-only contents access and retain their existing
behavior.
- Around line 12-16: Update the release workflow’s prerelease handling to derive
PRERELEASE from the validated SemVer prerelease component for both workflow
triggers, rather than trusting the manual prerelease input. Ensure versions such
as 0.1.0-alpha.1 are always marked as prereleases; alternatively, validate and
fail manual runs when the input conflicts with the version.
- Around line 241-254: Update the “Ensure release tag exists” step to validate
an existing tag’s dereferenced commit against HEAD using the tag-resolution
logic around git rev-parse. Keep the existing success path when they match, but
fail the workflow with a clear mismatch message before uploading when the tag
points to a different commit; retain tag creation and pushing for missing tags.
- Around line 37-60: Update the step identified by id "out" to pass
inputs.version via the RELEASE_VERSION environment variable and branch on
$GITHUB_EVENT_NAME, avoiding direct GitHub-expression interpolation in Bash.
Read the dispatch version from "$RELEASE_VERSION" before stripping an optional
v, and replace the current regex with strict SemVer validation that rejects
leading-zero numeric components, numeric prerelease identifiers with leading
zeros, and empty or repeated prerelease segments while preserving valid build
metadata.
- Around line 68-72: Update every uses reference in the release workflow,
including actions/checkout, pnpm/action-setup, and actions/setup-node, to a full
immutable commit SHA instead of a tag or branch ref. Add an adjacent comment
documenting each pinned action’s intended version, preserving the current
versions, and configure the repository’s automated dependency updater to
maintain these SHA pins.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cd44f141-3ee0-4c4b-9bd9-bec08f70e1cb
📒 Files selected for processing (15)
.github/workflows/release.ymlREADME.mdapps/api/package.jsonapps/desktop/README.mdapps/desktop/package.jsonapps/desktop/src-tauri/Cargo.tomlapps/desktop/src-tauri/tauri.conf.jsonapps/ui/package.jsonapps/ui/src/pages/SettingsPage.tsxdocs/DELIVERY.mdpackage.jsonpackages/db/package.jsonpackages/mcp/package.jsonpackages/mcp/src/index.tspackages/shared/package.json
| prerelease: | ||
| description: "Mark the GitHub Release as a pre-release" | ||
| required: true | ||
| type: boolean | ||
| default: true |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Derive prerelease status from the release version.
A manual run can publish 0.1.0-alpha.1 with prerelease: false. That release can then become /releases/latest, despite the desktop updater excluding prereleases.
Derive PRERELEASE from the validated SemVer prerelease component for both triggers. Alternatively, fail when the manual input conflicts with the version.
Also applies to: 41-52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 12 - 16, Update the release
workflow’s prerelease handling to derive PRERELEASE from the validated SemVer
prerelease component for both workflow triggers, rather than trusting the manual
prerelease input. Ensure versions such as 0.1.0-alpha.1 are always marked as
prereleases; alternatively, validate and fail manual runs when the input
conflicts with the version.
| permissions: | ||
| contents: write |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Restrict repository write permission to the publish job.
test and desktop do not create tags or releases. They currently receive contents: write.
Set the workflow default to contents: read. Grant contents: write only to publish.
Proposed fix
permissions:
- contents: write
+ contents: read
...
publish:
name: Publish GitHub Release
needs: [meta, desktop]
runs-on: ubuntu-latest
+ permissions:
+ contents: writeAlso applies to: 225-228
🧰 Tools
🪛 zizmor (1.29.0)
[error] 22-22: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level
(excessive-permissions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 21 - 22, Set the workflow-level
permissions default to contents: read, then add contents: write only within the
publish job. Ensure the test and desktop jobs inherit read-only contents access
and retain their existing behavior.
Source: Linters/SAST tools
| - id: out | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | ||
| VERSION="${{ inputs.version }}" | ||
| VERSION="${VERSION#v}" | ||
| PRERELEASE="${{ inputs.prerelease }}" | ||
| else | ||
| TAG_NAME="${GITHUB_REF_NAME}" | ||
| VERSION="${TAG_NAME#v}" | ||
| if [[ "$VERSION" == *"-"* ]]; then | ||
| PRERELEASE=true | ||
| else | ||
| PRERELEASE=false | ||
| fi | ||
| fi | ||
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$ ]]; then | ||
| echo "Invalid version: $VERSION" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*'
printf '%s\n' '--- release workflow outline/context ---'
wc -l .github/workflows/release.yml
cat -n .github/workflows/release.yml | sed -n '1,180p'
printf '%s\n' '--- relevant references ---'
rg -n -C 3 'inputs\.version|GITHUB_EVENT_NAME|GITHUB_REF_NAME|VERSION=|GITHUB_OUTPUT|package\.json|npm version|tag|release' .github/workflows/release.yml
printf '%s\n' '--- behavioral probe: generated Bash from the current interpolation ---'
python3 - <<'PY'
import subprocess
template = r'''set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
else
TAG_NAME="${GITHUB_REF_NAME}"
VERSION="${TAG_NAME#v}"
fi
printf 'VERSION=<%s>\n' "$VERSION"
'''
values = {
"benign": "v1.2.3",
"quote": '"; printf "INJECTED"; #',
"command_substitution": '$(printf INJECTED)',
"newline": '1.2.3\nprintf INJECTED\n#',
}
for name, value in values.items():
generated = template.replace("${{ inputs.version }}", value)
result = subprocess.run(
["bash", "-c", generated],
text=True,
capture_output=True,
env={"GITHUB_REF_NAME": "v9.9.9"},
)
print(f"[{name}] exit={result.returncode}")
print("stdout:", repr(result.stdout))
print("stderr:", repr(result.stderr))
PYRepository: Modsofthenation/PerformanceReviewManager
Length of output: 13281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import subprocess
current = r'''set -euo pipefail
if [[ "workflow_dispatch" == "workflow_dispatch" ]]; then
VERSION="${INPUT_VERSION}"
VERSION="${VERSION#v}"
printf 'VERSION=<%s>\n' "$VERSION"
fi
'''
direct_template = r'''set -euo pipefail
if [[ "workflow_dispatch" == "workflow_dispatch" ]]; then
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
printf 'VERSION=<%s>\n' "$VERSION"
fi
'''
env_template = r'''set -euo pipefail
if [[ "workflow_dispatch" == "workflow_dispatch" ]]; then
VERSION="${RELEASE_VERSION#v}"
printf 'VERSION=<%s>\n' "$VERSION"
fi
'''
values = {
"benign": "v1.2.3",
"command_substitution": '$(printf INJECTED)',
"quote_and_command": '"; printf INJECTED; #',
"newline_command": '1.2.3\nprintf INJECTED\n#',
}
for name, value in values.items():
direct = direct_template.replace("${{ inputs.version }}", value)
direct_result = subprocess.run(
["bash", "-c", direct],
text=True,
capture_output=True,
env={"PATH": "/usr/bin:/bin"},
)
env_result = subprocess.run(
["bash", "-c", env_template],
text=True,
capture_output=True,
env={"PATH": "/usr/bin:/bin", "RELEASE_VERSION": value},
)
print(f"[{name}] direct exit={direct_result.returncode}")
print(f"direct stdout={direct_result.stdout!r}")
print(f"direct stderr={direct_result.stderr!r}")
print(f"[{name}] env exit={env_result.returncode}")
print(f"env stdout={env_result.stdout!r}")
print(f"env stderr={env_result.stderr!r}")
PY
printf '%s\n' '--- current regex versus strict SemVer-shaped cases ---'
python3 - <<'PY'
import re
current = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$')
strict = re.compile(
r'^(0|[1-9][0-9]*)\.'
r'(0|[1-9][0-9]*)\.'
r'(0|[1-9][0-9]*)'
r'(?:-('
r'(?:0|[1-9][0-9]*|'
r'[0-9]*[A-Za-z-][0-9A-Za-z-]*'
r')(?:\.'
r'(?:0|[1-9][0-9]*|'
r'[0-9]*[A-Za-z-][0-9A-Za-z-]*'
r'))*)?'
r'(?:\+'
r'[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
)
cases = [
"1.2.3",
"0.1.0-alpha.1",
"1.2.3+build.7",
"1.2.3-alpha+build.7",
"01.2.3",
"1.2.3-01",
"1.2.3.",
"1.2.3.foo",
"1.2.3-alpha..1",
]
for value in cases:
print(f"{value:24} current={bool(current.fullmatch(value))} strict={bool(strict.fullmatch(value))}")
PYRepository: Modsofthenation/PerformanceReviewManager
Length of output: 1742
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
workflow_pattern = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$')
cases = [
("1.2.3", True),
("0.1.0-alpha.1", True),
("1.2.3+build.7", False),
("1.2.3-alpha+build.7", True),
("01.2.3", False),
("1.2.3-01", False),
("1.2.3.", False),
("1.2.3.foo", False),
("1.2.3-alpha..1", False),
]
for value, strict_semver_expected in cases:
accepted = bool(workflow_pattern.fullmatch(value))
print(f"{value:24} workflow_regex={accepted} strict_semver_expected={strict_semver_expected}")
PY
printf '%s\n' '--- exact Bash validation behavior ---'
for value in '1.2.3+build.7' '01.2.3' '1.2.3-01' '1.2.3.foo' '1.2.3-alpha..1'; do
if [[ "$value" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-].+)?$ ]]; then
result=accepted
else
result=rejected
fi
printf '%s: %s\n' "$value" "$result"
doneRepository: Modsofthenation/PerformanceReviewManager
Length of output: 987
Pass inputs.version through the step environment before validation.
inputs.version is inserted into Bash source at line 42. A value such as $(...) executes during assignment, and a quote can inject commands. Read the value from RELEASE_VERSION instead, and use $GITHUB_EVENT_NAME rather than interpolating github.event_name. Replace the current regex with strict SemVer validation; it accepts values such as 01.2.3, 1.2.3-01, and 1.2.3-alpha..1.
🧰 Tools
🪛 zizmor (1.29.0)
[error] 42-42: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 37 - 60, Update the step
identified by id "out" to pass inputs.version via the RELEASE_VERSION
environment variable and branch on $GITHUB_EVENT_NAME, avoiding direct
GitHub-expression interpolation in Bash. Read the dispatch version from
"$RELEASE_VERSION" before stripping an optional v, and replace the current regex
with strict SemVer validation that rejects leading-zero numeric components,
numeric prerelease identifiers with leading zeros, and empty or repeated
prerelease segments while preserving valid build metadata.
Source: Linters/SAST tools
| - uses: actions/checkout@v7 | ||
| - uses: pnpm/action-setup@v6 | ||
| with: | ||
| version: 10.33.3 | ||
| - uses: actions/setup-node@v7 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- workflow action references ---'
rg -n '^[[:space:]]*-[[:space:]]+uses:' .github/workflows/release.yml
printf '%s\n' '--- relevant workflow sections ---'
sed -n '1,280p' .github/workflows/release.ymlRepository: Modsofthenation/PerformanceReviewManager
Length of output: 10348
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json
import re
import urllib.request
from pathlib import Path
workflow = Path(".github/workflows/release.yml").read_text()
refs = re.findall(r'uses:\s*([^@\s]+)@([^\s#]+)', workflow)
for repo, ref in refs:
if "/" not in repo:
continue
if ref.startswith(("v", "stable")):
kind = "heads" if ref == "stable" else "tags"
url = f"https://api.github.com/repos/{repo}/git/ref/{kind}/{ref}"
try:
with urllib.request.urlopen(url) as response:
data = json.load(response)
obj = data.get("object", {})
print(f"{repo}@{ref}: {kind}, type={obj.get('type')}, sha={obj.get('sha')}")
except Exception as exc:
print(f"{repo}@{ref}: lookup failed: {exc}")
else:
print(f"{repo}@{ref}: non-version ref")
PYRepository: Modsofthenation/PerformanceReviewManager
Length of output: 2454
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json
import re
import subprocess
from pathlib import Path
refs = sorted(set(re.findall(
r'uses:\s*([^@\s]+)@([^\s#]+)',
Path(".github/workflows/release.yml").read_text(),
)))
for repo, ref in refs:
endpoint = "heads" if ref == "stable" else "tags"
url = f"https://api.github.com/repos/{repo}/git/ref/{endpoint}/{ref}"
result = subprocess.run(
["curl", "-kfsSL", "-H", "Accept: application/vnd.github+json", url],
text=True, capture_output=True, check=False,
)
try:
data = json.loads(result.stdout)
obj = data.get("object", {})
print(json.dumps({
"uses": f"{repo}@{ref}",
"ref_type": endpoint,
"object_type": obj.get("type"),
"object_sha": obj.get("sha"),
"error": data.get("message"),
}, sort_keys=True))
except Exception:
print(json.dumps({
"uses": f"{repo}@{ref}",
"error": result.stderr.strip() or result.stdout[:200],
}, sort_keys=True))
PYRepository: Modsofthenation/PerformanceReviewManager
Length of output: 1570
Pin all release-pipeline actions to immutable commit SHAs.
The workflow uses version tags and the stable branch. An upstream ref change can execute unreviewed code with contents: write. Replace every uses: reference with a full commit SHA. Keep the intended version in an adjacent comment and use an automated dependency updater for future updates.
🧰 Tools
🪛 zizmor (1.29.0)
[warning] 68-68: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 72-72: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): this step
(cache-poisoning)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 68 - 72, Update every uses
reference in the release workflow, including actions/checkout,
pnpm/action-setup, and actions/setup-node, to a full immutable commit SHA
instead of a tag or branch ref. Add an adjacent comment documenting each pinned
action’s intended version, preserving the current versions, and configure the
repository’s automated dependency updater to maintain these SHA pins.
| - name: Ensure release tag exists | ||
| if: github.event_name == 'workflow_dispatch' | ||
| env: | ||
| TAG: ${{ needs.meta.outputs.tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| if git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| echo "Tag $TAG already exists" | ||
| else | ||
| git tag -a "$TAG" -m "Release $TAG" | ||
| git push origin "$TAG" | ||
| fi |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Verify the existing tag points to the built commit.
Line 249 only checks whether the tag exists. A manual run from another ref can upload new installers to a release whose tag identifies an older commit.
If the tag exists, compare its dereferenced commit with HEAD and fail on mismatch.
Proposed fix
- if git rev-parse "$TAG" >/dev/null 2>&1; then
- echo "Tag $TAG already exists"
+ if git rev-parse -q --verify "refs/tags/$TAG^{commit}" >/dev/null; then
+ TAG_COMMIT="$(git rev-list -n 1 "$TAG")"
+ HEAD_COMMIT="$(git rev-parse HEAD)"
+ if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then
+ echo "Tag $TAG points to $TAG_COMMIT, but this build uses $HEAD_COMMIT" >&2
+ exit 1
+ fi
+ echo "Tag $TAG already exists at the built commit"
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Ensure release tag exists | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| TAG: ${{ needs.meta.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "Tag $TAG already exists" | |
| else | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| fi | |
| - name: Ensure release tag exists | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| TAG: ${{ needs.meta.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]`@users.noreply.github.com`" | |
| if git rev-parse -q --verify "refs/tags/$TAG^{commit}" >/dev/null; then | |
| TAG_COMMIT="$(git rev-list -n 1 "$TAG")" | |
| HEAD_COMMIT="$(git rev-parse HEAD)" | |
| if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then | |
| echo "Tag $TAG points to $TAG_COMMIT, but this build uses $HEAD_COMMIT" >&2 | |
| exit 1 | |
| fi | |
| echo "Tag $TAG already exists at the built commit" | |
| else | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 241 - 254, Update the “Ensure
release tag exists” step to validate an existing tag’s dereferenced commit
against HEAD using the tag-resolution logic around git rev-parse. Keep the
existing success path when they match, but fail the workflow with a clear
mismatch message before uploading when the tag points to a different commit;
retain tag creation and pushing for missing tags.
Summary
Adds a formal Release GitHub Actions workflow and bumps the project to
0.1.0-alpha.1so we can publish the first alpha with desktop installers attached.Why
Desktop builds already exist via manual Desktop Installers
workflow_dispatch, but they only upload Actions artifacts (30-day retention). There is no GitHub Release yet, and Settings → Check for desktop updates looks at/releases/latest.This PR wires a release pipeline that:
v{version}if neededHow to cut the alpha (after merge)
0.1.0-alpha.1Alternatively:
git tag v0.1.0-alpha.1 && git push origin v0.1.0-alpha.1onmain(hyphenated tags are treated as pre-releases).Notes
mainafter Desktop Installers was split out of CI; docs now reference both Desktop Installers (artifacts) and Release (GitHub Release).403 Resource not accessible by integration), so a human (or aGH_PATwith Actions + Contents write) must trigger the workflow after merge./releases/latestignores pre-releases; once a non-alpha release exists, Settings update checks will pick that up.Summary by CodeRabbit
New Features
Documentation
Chores
0.1.0-alpha.1.